import { ApiCall, BaseCall, BaseConnection, BaseServer, BaseServiceType, FlowNode, MsgCall, TsrpcError, WsServer, WsServerOptions } from "tsrpc";
import { apiErrorThenClose } from "../ApiBase";
import { getRedisClient, RedisClient } from "../redisHelper";
import { MsgClusterSyncNodeInfo } from "./protocols/MsgClusterSyncNodeInfo";
import { MsgAssignTask } from "./protocols/MsgAssignTask";
import { ReqClusterLogin, ResClusterLogin } from "./protocols/PtlClusterLogin";
import { ServiceType as ClusterServiceType, serviceProto as clusterServiceProto } from "./protocols/serviceProto";


declare module 'tsrpc' {
    export interface BaseConnection {
        nodeId: string;
    }
}

/**集群节点*/
interface IClusterNode<NodeInfo> {
    nodeInfo: IClusterNodeInfo<NodeInfo>;
    nodeConn: BaseConnection<ClusterServiceType>;
}
/**集群节点信息*/
export interface IClusterNodeInfo<NodeInfo> {
    nodeId: string;
    nodeInfo: NodeInfo;
    expires: Date;
}
/**集群节点配置*/
export interface IClusterNodeCfg {
    nodeId: string;
    clusterKey: string;
}


/**
 * 集群管理类
 * @date 2022/4/19 - 16:48:53
 *
 * @export
 * @class ClusterMgr
 * @typedef {ClusterMgr}
 * @template NodeInfo 节点信息的类型，可自定义
 */
export class ClusterMgr<NodeInfo = any>{

    /**集群类型标识*/
    clusterTypeKey: string;

    /**获取所有可以加入集群的配置 */
    getNodesCfg: () => IClusterNodeCfg[];

    /**服务*/
    server: WsServer<ClusterServiceType>;

    /**所有节点，nodeId=>IClusterNode*/
    nodes: Map<string, IClusterNode<NodeInfo>> = new Map<string, IClusterNode<NodeInfo>>();

    /**
     * Creates an instance of ClusterMgr.
     * @date 2022/4/20 - 14:20:23
     *
     * @constructor
     * @param {string} clusterTypeKey 集群类型标识，用在各种场合进行区分的，需要唯一定义
     * @param {() => IClusterNodeCfg[]} getNodesCfg
     * @param {Partial<WsServerOptions<ClusterServiceType>>} serverOption
     */
    constructor(clusterTypeKey: string, getNodesCfg: () => IClusterNodeCfg[], serverOption: Partial<WsServerOptions<ClusterServiceType>>) {
        this.clusterTypeKey = clusterTypeKey;
        this.getNodesCfg = getNodesCfg;
        this.server = new WsServer(clusterServiceProto, serverOption);

        //所有消息和api请求，都必须在认证通过之后
        this.server.flows.preApiCallFlow.push((call) => {
            if (!this.prefixCheck(call)) {
                apiErrorThenClose(call, 'need Login before do this', { code: 4000 });
                return;
            }
            return call;
        });
        this.server.flows.preMsgCallFlow.push((call) => {
            if (!this.prefixCheck(call)) {
                call.logger.error(`need Login before do this (${call.service.name}, msg:${JSON.stringify(call.msg)})`);
                call.conn.close();
                return;
            }
            return call;
        });
        this.server.implementApi("ClusterLogin", async (call) => {
            await this.apiLogin(call);
        });
        this.server.listenMsg("ClusterSyncNodeInfo", async call => {
            await this.msgSyncNodeInfo(call);
        });
        this.server.flows.postDisconnectFlow.push((v) => {
            this.nodeDisconnect(v.conn);
            return v;
        });
    }

    public async start() {
        await this.server.start();
    }
    public async stop() {
        await this.server.stop();
    }

    private prefixCheck(call: BaseCall<ClusterServiceType>): boolean {
        if (call.service.name == "ClusterLogin") {
            return true;
        }
        if (!call.conn.nodeId) {
            return false;
        }
        return true;
    }
    private async apiLogin(call: ApiCall<ReqClusterLogin, ResClusterLogin, any>) {
        var cfg = this.getNodesCfg().find(c => c.nodeId === call.req.nodeId);
        if (!cfg) {
            return await apiErrorThenClose(call, `认证失败！不存在nodeId=${call.req.nodeId}`);
        }
        if (cfg.clusterKey !== call.req.clusterKey) {
            return await apiErrorThenClose(call, `认证失败！错误的clusterKey`);
        }

        let node: IClusterNode<NodeInfo> = {
            nodeInfo: {
                nodeId: call.req.nodeId,
                nodeInfo: call.req.nodeInfo,
                expires: new Date(),
            },
            nodeConn: call.conn,
        };
        this.nodes.set(node.nodeInfo.nodeId, node);
        this.syncNodeInfoToRedis(node.nodeInfo);

        call.conn.nodeId = node.nodeInfo.nodeId;

        await call.succ({});
    }
    private async msgSyncNodeInfo(call: MsgCall<MsgClusterSyncNodeInfo, any>) {

        var node = this.nodes.get(call.conn.nodeId);
        if (!node) {
            call.logger.error(`连接错误将被踢出`);
            call.conn.close();
            return;
        }

        node.nodeInfo.nodeInfo = call.msg.nodeInfo;
        node.nodeInfo.expires = new Date();
        this.syncNodeInfoToRedis(node.nodeInfo);
    }
    private nodeDisconnect(conn: BaseConnection<ClusterServiceType>) {
        this.nodes.delete(conn.nodeId);
        this.delNodeInfoToRedis(conn.nodeId);
    }

    private async syncNodeInfoToRedis(nodeInfo: IClusterNodeInfo<NodeInfo>) {
        await (await getRedisClient()).setHashObject(
            `ClusterMgr:${this.clusterTypeKey}:Nodes`,
            nodeInfo.nodeId, nodeInfo);
    }
    private async delNodeInfoToRedis(nodeId: string) {
        await (await getRedisClient()).removeHashValue(`ClusterMgr:${this.clusterTypeKey}:Nodes`, nodeId);
    }


    /**
     * 从redis中获取所有节点列表, 分布式时，gate服务器和游戏服务器管理节点，可能不是同个实例，所以使用本方法来获取
     * @date 2022/4/20 - 16:48:25
     *
     * @public
     * @static
     * @async
     * @template NodeInfo
     * @param {string} clusterTypeKey 集群类型标识，用在各种场合进行区分的。需要和构造ClussterMgr时的值一致
     * @returns {Promise<IClusterNodeInfo<NodeInfo>[]>}
     */
    public static async getNodeInfosFromRedis<NodeInfo>(clusterTypeKey: string): Promise<IClusterNodeInfo<NodeInfo>[]> {
        var allKv = await (await getRedisClient()).getHashObjects<IClusterNodeInfo<NodeInfo>>(`ClusterMgr:${clusterTypeKey}:Nodes`);
        var nodes: IClusterNodeInfo<NodeInfo>[] = [];
        var now = new Date();
        for (var key in allKv) {
            var nodeInfo = allKv[key];
            if (nodeInfo.expires < now) continue;
            nodes.push(nodeInfo);
        }
        return nodes;
    }

    /*
    private async assignTask(ResAssignTask msg) {
        
        await call.succ({});
    }
*/

}

